package com.company;

import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;


public class LinkedBinaryTree<T> implements Iterable<T>, BinaryTreeADT<T> {


    protected BinaryTreeNode<T> root;
    protected int modCount;
    protected LinkedBinaryTree<T> left,right;

    public LinkedBinaryTree() {
        root = null;
    }

    public LinkedBinaryTree(T element) {
        root = new BinaryTreeNode<T>(element);
    }

    public LinkedBinaryTree(T element, LinkedBinaryTree<T> left,
                            LinkedBinaryTree<T> right) {
        root = new BinaryTreeNode<T>(element);
        root.setLeft(left.root);
        root.setRight(right.root);
        this.left = left;
        this.right=right;
    }


    public BinaryTreeNode<T> getRootNode() throws EmptyCollectionException {
        if (isEmpty()) {
            throw new EmptyCollectionException("BinaryTreeNode ");
        }
        return root;
    }

    public LinkedBinaryTree<T> getLeft() {
        return left;

    }

    public LinkedBinaryTree<T> getRight() {
        return right;
    }

    public int getHeight()
    {
        return height(root);
    }

    private int height(BinaryTreeNode<T> node)
    {
        if(node==null){
            return 0;
        }
        else {
            int leftTreeHeight = height(node.getLeft());
            int rightTreeHeight= height(node.getRight());
            return leftTreeHeight>rightTreeHeight ? (leftTreeHeight+1):(rightTreeHeight+1);
        }
    }

    @Override
    public T getRootElement() throws EmptyCollectionException {
        if (root.getElement().equals(null)) {
            throw new EmptyCollectionException("BinaryTreeNode ");
        }
        return root.getElement();
    }

    @Override
    public boolean isEmpty() {
        return (root == null);
    }

    @Override
    public int size() {

        int size = 0;
        if(root.getLeft()!=null){
            size+=1;
        }
        if(root.getRight()!=null){
            size+=1;
        }

        return size;
    }

    public String removeRightSubtree() throws EmptyCollectionException {
        if (root == null)
            throw new EmptyCollectionException("tree is empty");
        BinaryTreeNode cur = root;
        while (cur.getLeft() != null){
            cur.setRight(null);
            cur = cur.left;
        }
        return super.toString();
    }

    public void removeAllElements() throws EmptyCollectionException {
        if (root == null)
            throw new EmptyCollectionException("tree is empty");
        root = null;
    }

    @Override
    public boolean contains(T targetElement) {
        if(targetElement == find(targetElement))
            return true;
        else
            return false;
    }

    @Override
    public T find(T targetElement) {
        BinaryTreeNode<T> current = findAgain(targetElement, root);

        if (current == null)
            try {
                throw new ElementException("LinkedBinaryTree");
            } catch (ElementException e) {
                e.printStackTrace();
            }

        return (current.getElement());
    }

    private BinaryTreeNode<T> findAgain(T targetElement, BinaryTreeNode<T> next) {
        if (next == null)
            return null;

        if (next.getElement().equals(targetElement))
            return next;
        BinaryTreeNode<T> temp = findAgain(targetElement, next.getLeft());

        if (temp == null)
            temp = findAgain(targetElement, next.getRight());

        return temp;
    }

    @Override
    public Iterator<T> iteratorInOrder() {
        ArrayListNode<T> tempList = new ArrayListNode<T>();
        inOrder(root, tempList);
        return new TreeIterator(tempList.iterator());
    }

    public void toPreString(){
        preOrder(root);
    }
    private void preOrder(BinaryTreeNode root){
        if(null!= root){
            System.out.print(root.getElement() + "\t");
            preOrder(root.getLeft());
            preOrder(root.getRight());
        }
    }

    public void toPostString(){
        postOrder(root);
    }
    private void postOrder(BinaryTreeNode root) {
        if (null != root) {
            postOrder(root.getLeft());
            postOrder(root.getRight());
            System.out.print(root.getElement() + "\t");
        }
    }

    protected void inOrder(BinaryTreeNode<T> node, ArrayListNode<T> tempList) {
        if (node != null) {
            inOrder(node.getLeft(), tempList);
            tempList.addToRear(node.getElement());
            inOrder(node.getRight(), tempList);
        }
    }

    @Override
    public Iterator<T> iteratorPreOrder() {
        ArrayListNode<T> tempList = new ArrayListNode<T>();
        preOrder(root, tempList);
        return new TreeIterator(tempList.iterator());
    }
    private void preOrder(BinaryTreeNode<T> node, ArrayListNode<T> tempList) {
        if (node != null) {
            tempList.addToRear(node.getElement());
            inOrder(node.getLeft(), tempList);
            inOrder(node.getRight(), tempList);
        }
    }

    @Override
    public Iterator<T> iteratorPostOrder() {
        ArrayListNode<T> tempList = new ArrayListNode<T>();
        postOrder(root, tempList);
        return new TreeIterator(tempList.iterator());
    }
    private void postOrder(BinaryTreeNode<T> node, ArrayListNode<T> tempList) {
        if (node != null) {
            tempList.addToRear(node.getElement());
            inOrder(node.getLeft(), tempList);
            inOrder(node.getRight(), tempList);
        }
    }

    @Override
    public Iterator<T> iteratorLevelOrder() throws EmptyCollectionException {
        ArrayListNode<BinaryTreeNode<T>> nodes = new ArrayListNode<BinaryTreeNode<T>>();
        ArrayListNode<T> tempList = new ArrayListNode<T>();
        BinaryTreeNode<T> current;

        nodes.addToRear(root);

        while (!nodes.isEmpty()) {
            current = nodes.removeFirst();

            if (current != null) {
                tempList.addToRear(current.getElement());
                System.out.println(current.element);
                if (current.getLeft() != null)
                    nodes.addToRear(current.getLeft());
                System.out.println(current.left);
                if (current.getRight() != null)
                    nodes.addToRear(current.getRight());
                System.out.println(current.right);
            } else
                tempList.addToRear(null);
        }

        return new TreeIterator(tempList.iterator());
    }

    public void toLevelString1(){
        if(root == null)
            return;
        int height = getHeight();
        for(int i = 1; i <= height; i++){
            levelOrder(root,i);
        }
    }
    private void levelOrder(BinaryTreeNode root,int level){
        if(root == null || level < 1){
            return;
        }
        if(level == 1){
            System.out.print(root.getElement() + "\n");
            return;
        }
        levelOrder(root.getLeft(),level - 1);
        levelOrder(root.getRight(),level - 1);
    }

    @Override
    public Iterator<T> iterator() {
        return iteratorInOrder();
    }

    public String toString() {
        UnorderedListADT<BinaryTreeNode<T>> nodes = new ArrayListNode<>();
        UnorderedListADT<Integer> levelList = new ArrayListNode<>();

        BinaryTreeNode<T> current = null;
        String result = "";
        int printDepth = this.getHeight();
        int possibleNodes = (int) Math.pow(2, printDepth + 1);
        int countNodes = 0;

        nodes.addToRear(root);
        Integer currentLevel = 0;
        Integer previousLevel = -1;
        levelList.addToRear(currentLevel);

        while (countNodes < possibleNodes) {
            countNodes = countNodes + 1;
            try {
                current = nodes.removeFirst();
            } catch (EmptyCollectionException e) {
                e.printStackTrace();
            }
            try {
                currentLevel = levelList.removeFirst();
            } catch (EmptyCollectionException e) {
                e.printStackTrace();
            }
            if (currentLevel > previousLevel) {
                result = result + "\n\n";
                previousLevel = currentLevel;
                for (int j = 0; j < ((Math.pow(2, (printDepth - currentLevel))) - 1); j++)
                    result = result + " ";
            } else {
                for (int i = 0; i < (Math.pow(2, (printDepth - currentLevel + 1)) - 1); i++) {
                    result = result + " ";
                }
            }
            if (current != null) {
                result = result + (current.getElement()).toString();
                nodes.addToRear(current.getLeft());
                levelList.addToRear(currentLevel + 1);
                nodes.addToRear(current.getRight());
                levelList.addToRear(currentLevel + 1);
            } else {
                nodes.addToRear(null);
                levelList.addToRear(currentLevel + 1);
                result = result + " ";
            }
        }
        return result;
    }

    private class TreeIterator implements Iterator<T> {
        private int expectedModCount;
        private Iterator<T> iter;

        public TreeIterator(Iterator<T> iter) {
            this.iter = iter;
            expectedModCount = modCount;
        }

        public boolean hasNext() throws ConcurrentModificationException {
            if (!(modCount == expectedModCount))
                throw new ConcurrentModificationException();

            return (iter.hasNext());
        }

        public T next() throws NoSuchElementException {
            if (hasNext())
                return (iter.next());
            else
                throw new NoSuchElementException();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

}